Skip to content

C++11 新特性

C++11 的特性主要包括下⾯⼏个⽅⾯:

  • 提⾼运⾏效率的语⾔特性:右值引⽤、泛化常量表达式
  • 原有语法的使⽤性增强:初始化列表、统⼀的初始化语法、类型推导、范围 for 循环、Lambda 表达式、final 和 override、构造函数委托
  • 语⾔能⼒的提升:空指针 nullptr、default 和 delete、⻓整数、静态 assert
  • C++ 标准库的更新:智能指针、正则表达式、哈希表等

1、空指针 nullptr

nullptr 出现的⽬的是为了替代 NULL。

在某种意义上来说,传统 C++ 会把 NULL、 0 视为同⼀种东⻄,这取决于编译器如何定义NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义为 0。C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译 char *ch= NULL; 时,NULL 只好被定义为 0。⽽这依然会产⽣问题,将导致了 C++ 中重载特性会发⽣混乱,考虑:

cpp
voidfunc(int);
voidfunc(char*);

对于这两个函数来说,如果 NULL ⼜被定义为了 0 那么 func(NULL) 这个语句将会去调⽤func(int),从⽽导致代码违反直观。

为了解决这个问题,C++11 引⼊了 nullptr 关键字,专⻔⽤来区分空指针、0。nullptr 的类型为nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进⾏相等或者不等的⽐较。

当需要使⽤ NULL 时候,养成直接使⽤ nullptr 的习惯。

2、Lambda 表达式

Lambda表达式实际上就是提供了⼀个类似匿名函数的特性,⽽匿名函数则是在需要⼀个函数,但是⼜不想费⼒去命名⼀个函数的情况下去使⽤的。

利⽤lambda表达式可以编写内嵌的匿名函数,⽤以替换独⽴函数或者函数对象,并且使代码更可读。

从本质上来讲,lambda表达式只是⼀种语法糖,因为所有其能完成的⼯作都可以⽤其它稍微复杂的代码来实现,但是它简便的语法却给 C++ 带来了深远的影响。

从⼴义上说,lamdba表达式产⽣的是函数对象。在类中,可以重载函数调⽤运算符(),此时类的对象可以将具有类似函数的⾏为,我们称这些对象为函数对象(Function Object)或者仿函数(Functor)。相⽐ lambda表达式,函数对象有⾃⼰独特的优势。

lambda表达式⼀般都是从⽅括号[]开始,然后结束于花括号{},花括号⾥⾯就像定义函数那样,包含了lamdba表达式体,⼀个最简单的例⼦如下:

cpp
// 定义简单的lambda表达式
auto basicLambda = [] { cout << "Hello, world!" << endl; };
basicLambda(); // 输出:Hello, world!

上⾯是最简单的lambda表达式,没有参数。如果需要参数,那么就要像函数那样,放在圆括号⾥⾯,如果有返回值,返回类型要放在->后⾯,即拖尾返回类型,当然你也可以忽略返回类型,lambda会帮你⾃动推断出返回类型:

cpp
// 指明返回类型,托尾返回类型
auto add = [](int a, int b) -> int { return a + b; };
// ⾃动推断返回类型
auto multiply = [](int a, int b) { return a * b; };

int sum = add(2, 5); // 输出:7
int product = multiply(2, 5); // 输出:10

最前边的[]是lambda表达式的⼀个很重要的功能,就是闭包。

先说明⼀下lambda表达式的⼤致原理:每当你定义⼀个lambda表达式后,编译器会⾃动⽣成⼀个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。

那么在运⾏时,这个lambda表达式就会返回⼀个匿名的闭包实例,其实⼀个右值。所以,我们上⾯的lambda表达式的结果就是⼀个个闭包实例。

闭包的⼀个强⼤之处是其可以通过传值或者引⽤的⽅式捕捉其封装作⽤域内的变量,前⾯的⽅括号就是⽤来定义捕捉模式以及变量,我们⼜将其称为lambda捕捉块。例⼦如下:

cpp
int main() {
	int x = 10;
  auto add_x = [x](int a) { return a + x; }; // 复制捕捉x,lambda表达式⽆法修改此变量
  auto multiply_x = [&x](int a) { return a * x; }; // 引⽤捕捉x,lambda表达式可以修改此变量
  cout << add_x(10) << " " << multiply_x(10) << endl;
  // 输出:20 100
  return 0;
}

捕获的⽅式可以是引⽤也可以是复制,但是具体说来会有以下⼏种情况来捕获其所在作⽤域中的变量:

  • []:默认不捕获任何变量;

  • [=]:默认以值捕获所有变量;

  • [&]:默认以引⽤捕获所有变量;

  • [x]:仅以值捕获x,其它变量不捕获;

  • [&x]:仅以引⽤捕获x,其它变量不捕获;

  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引⽤捕获;

  • [&, x]:默认以引⽤捕获所有变量,但是x是例外,通过值捕获;

  • [this]:通过引⽤捕获当前对象(其实是复制指针);

  • [*this]:通过传值⽅式捕获当前对象;

lambda表达式⼀个更重要的应⽤是其可以⽤于函数的参数,通过这种⽅式可以实现回调函数。其实,最常⽤的是在STL算法中,⽐如你要统计⼀个数组中满⾜特定条件的元素数量,

通过lambda表达式给出条件,传递给count_if函数:

cpp
int val = 3;
vector<int> v {1, 8, 5, 3, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [val](int x) { return x > val; });
// v中⼤于3的元素数量

最后给出lambda表达式的完整语法:

cpp
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17)
exception attribute -> ret { body }

// 可选的简化语法
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
  • capture-list:捕捉列表,这个不⽤多说,前⾯已经讲过,它不能省略;

  • params:参数列表,可以省略(但是后⾯必须紧跟函数体);

  • mutable:可选,将lambda表达式标记为mutable后,函数体就可以修改传值⽅式捕获的变量;

    lambda 表达式默认以 const值拷贝的方式捕获变量,加上 mutable后,就可以修改这些拷贝过来的副本了。

  • constexpr:可选,C++17,可以指定lambda表达式是⼀个常量函数;

  • exception:可选,指定lambda表达式可以抛出的异常;

  • attribute:可选,指定lambda表达式的特性;

    • [[noreturn]]:告诉编译器:这个 lambda 永远不会正常返回(一定会抛异常或终止)。
    • [[nodiscard]](C++17+)。提示调用方:返回值不应被忽略
    • [[maybe_unused]]。抑制未使用参数/变量的警告(对 lambda 参数有用)
    • [[deprecated]]。标记 lambda 已废弃
  • ret:可选,返回值类型;

  • body:函数执⾏体。

3、右值引⽤

C++03 及之前的标准中,右值是不允许被改变的,实践中也通常使⽤const T&的⽅式传递右值。然⽽这是效率低下的做法,例如:

cpp
Person get(){
  Person p;
  return p;
}
Person p = get();

上述获取右值并初始化p的过程包含了Person的3个构造过程和2个析构过程。这是 C++ ⼴受诟病的⼀点,但C++11 的右值引⽤特性允许我们对右值进⾏修改。借此可以实现move语义,即从右值中直接拿数据过来初始化或修改左值,⽽不需要重新构造左值后再析构右值。

⼀个move构造函数是这样声明的:

cpp
class Person{
public:
  Person(Person&& rhs){...}
  ...
};

4、泛化的常量表达式

还记得刚开始学习 C++ 给你的苦恼吗?你看:

cpp
int N = 5;
int arr[N];

编译器会报错:error: variable length array declaration not allowed at filescope int arr[N];,但N就是5,不过编译器不知道这⼀点,于是我们需要声明为const int N = 5才可以。但C++11的泛化常数给出了解决⽅案:

cpp
constexpr int N = 5; // N 变成了⼀个只读的值
int arr[N];             // OK

constexpr告诉编译器这是⼀个编译期常量,甚⾄可以把⼀个函数声明为编译期常量表达式。

cpp
constexpr int getFive(){ return 5; }
int arr[getFive() + 1];

5、初始化列表

接下来⼏个特性属于原有语⾔特性的使⽤性增强。这意味着这些操作原来也是可以实现的,不过现在语法上更加简洁。⽐如⾸先要介绍的初始化列表。

⽽ C++11 提供了initializer_list来接受变⻓的对象初始化列表:

cpp
classA{
public:
	A(std::initializer_list<int> list);
};
A a = {1, 2, 3};

注意初始化列表特性只是现有语法增强,并不是提供了动态的可变参数。该列表只能静态地构造。

6、统⼀的初始化语法

不同的数据类型具有不同的初始化语法。如何初始化字符串?如何初始化数组?如何初始化多维数组?如何初始化对象?

C++11给出了统⼀的初始化语法:均可使⽤“{}-初始化变量列表”:

cpp
X x1 = X{1,2};
X x2 = {1,2}; // 此处的'='可有可⽆
X x3{1,2};
X* p = new X{1,2};
struct D : X {
	D(int x, int y) :X{x,y} { /* … */ };
};
struct S {
	int a[3];
  // 对于旧有问题的解决⽅案
  S(int x, int y, int z) :a{x,y,z} { /* … */ };
};

7、类型推导

C++ 提供了auto和decltype来静态推导类型,在我们知道类型没有问题但⼜不想完整地写出类型的时候,便可以使⽤静态类型推导。

cpp
for(vector<int>::const_iterator it = v.begin(); it != v.end(); ++it);
// 可以改写为
for(auto it = v.begin(); it != v.end(); ++it);

虽然写起来和动态语⾔(如JavaScript的var)很像,但C++仍然是强类型的,会执⾏静态类型检查的语⾔。这只是语法上的简化,并未改变C++的静态类型检查。

decltype⽤于获取⼀个表达式的类型,⽽不对表达式进⾏求值(类似于sizeof)。decltyp(e)规则如下:

  • 若e为⼀个⽆括号的变量、函数参数、类成员,则返回类型为该变量/参数/类成员在源程序中的声明类型;
  • 否则的话,根据表达式的分类(value categories),设T为e的类型:
    • 若e是⼀个左值(lvalue,即“可寻址值”),返回T&
    • 若e是⼀个临终值(xvalue),则返回值为T&&
    • 若e是⼀个纯右值(prvalue),则返回值为T
cpp
const std::vector<int> v(1);
const int&& foo(); // 返回临终值:⽣命周期已结束但内存还未拿⾛

auto a = v[0]; // a 为 int
decltype(v[0]) b = 0; // b 为 const int&// 即 vector<int>::operator[](size_type) const 的返回值类型

auto c = 0; // c, d 均为 int
auto d = c;
decltype(c) e; // e 为 int,即 c 的类型
decltype((c)) f = e; // f 为 int&,因为 c 是左值
decltype(0) g; // g 为 int,因为 0 是右值

8、基于范围的for循环

Boost 中定义了很多"范围",很多标准库函数都使⽤了范围⻛格的实现。这⼀概念被C++11提了出来:

cpp
int arr[5];
std::vector<int> v;

for(int x: arr);
for(const int& x: arr);
for(int x: v);
for(auto &x: v);

9、构造函数委托

在 C# 和 Java 中,⼀个构造函数可以调⽤另⼀个来实现代码复⽤,但 C++⼀直不允许这样做。

现在可以了,这使得构造函数可以在同⼀个类中⼀个构造函数调⽤另⼀个构造函数,从⽽达到简化代码的⽬的:

cpp
class myBase {
  int number; string name;
  myBase( int i, string& s ) : number(i), name(s){}
public:
  myBase( )           : myBase( 0, "invalid" ){}
  myBase( int i )     : myBase( i, "guest" ){}
  myBase( string& s ) : myBase( 1, s ){ PostInit(); }
};

10、final 和 override

C++ 借由虚函数实现运⾏时多态,但 C++ 的虚函数⼜很多脆弱的地⽅:

  • ⽆法禁⽌⼦类重写它。可能到某⼀层级时,我们不希望⼦类继续来重写当前虚函数了。
  • 容易不⼩⼼隐藏⽗类的虚函数。⽐如在重写时,不⼩⼼声明了⼀个签名不⼀致但有同样名称的新函数

C++11 提供了final来禁⽌虚函数被重写/禁⽌类被继承override来显示地重写虚函数。这样编译器给我们不⼩⼼的⾏为提供更多有⽤的错误和警告。

cpp
struct Base1 final { };
struct Derived1 : Base1 {}; // 编译错:Base1不允许被继承

struct Base2 {
  virtual void f1() final;
  virtual void f2();
};

struct Derived2 : Base2 {
  virtual void f1(); // 编译错:f1不允许重写
  virtual void f2(int) override; // 编译错:⽗类中没有 void f2(int)
  };

11、default 和 delete

我们知道编译器会为类⾃动⽣成⼀些⽅法,⽐如构造和析构函数(完整的列表⻅ EffectiveC++: Item 5)。

现在我们可以显式地指定和禁⽌这些⾃动⾏为了。

cpp
struct classA {
  classA() = default; // 声明⼀个⾃动⽣成的函数
  classA(T value);
  void *operator new(size_t) = delete; // 禁⽌⽣成new运算符
};

在上述classA中定义了classA(T value)构造函数,因此编译器不会默认⽣成⼀个⽆参数的构造函数了,如果我们需要可以⼿动声明,或者直接= default。

12、静态 assertion

C++ 提供了两种⽅式来assert:⼀种是assert宏,另⼀种是预处理指令#error。前者在运⾏期起作⽤,⽽后者是预处理期起作⽤。它们对模板都不好使,因为模板是编译期的概念。

static_assert关键字的使⽤⽅式如下:

cpp
template< classT >
struct Check {
	static_assert( sizeof(int) <= sizeof(T), "T is not big enough!" ) ;
} ;

13、智能指针

接下来介绍 C++11 对于 C++ 标准库的变更。C++11 把 TR1 并⼊了进来,废弃了 C++98 中的auto_ptr,同时将shared_ptr和uniq_ptr并⼊std命名空间。

智能指针在 [Effective C++: Item 13] 中已经有不少讨论了。这⾥给⼀个例⼦:

cpp
int main(){
	std::shared_ptr<double> p_first(newdouble);
	{
		std::shared_ptr<double> p_copy = p_first;
		*p_copy = 21.2;
  } // p_copy 被销毁,⾥⾯的 double 还有⼀个引⽤因此仍然保持
  return0; // p_first 及其⾥⾯的 double 销毁
  }

14、正则表达式

这个任何⼀⻔现代的编程语⾔都会提供的特性终于进标准:

cpp
const char* reg_esp = "[ ,.\\t\\n;:]";
std::regex rgx(reg_esp) ;
std::cmatch match ;
const char*target = "Polytechnic University of Turin " ;

if( regex_search( target, match, rgx ) ) {
  const size_t n = match.size();
  for( size_t a = 0 ; a < n ; a++ ) {
    string str( match[a].first, match[a].second ) ;
    cout << str << "\n" ;
  }
}

上述代码段来⾃Wikipedia: https://zh.wikipedia.org/wiki/C%2B%2B11

15、增强的元组

在 C++ 中本已有⼀个pair模板可以定义⼆元组,C++11 更进⼀步地提供了边⻓参数的tuple模板:

cpp
typedef std::tuple< int , double, string       > tuple_1 t1;
typedef std::tuple< char, short , const char * > tuple_2 t2 ('X', 2,"Hola!");
t1 = t2 ; // 隐式类型转换

16、哈希表

C++ 的map, multimap, set, multiset使⽤红⿊树实现,插⼊和查询都是 O(lgn) 的复杂度,但 C++11 为这四种模板类提供了(底层哈希实现)以达到 O(1) 的复杂度:

散列表类型有⽆关系值接受相同键值

散列表类型有无关系值接受相同键值
std::unordered_set
std::unordered_multiset
std::unordered_map
std::unordered_multimap

参考:https://harttle.land/2015/10/08/cpp11.html

C++11 新特性 图 67

用心记录,持续成长